CORS에 대하여

CORS 많이 들어보셨죠?

개발하다보면 CORS 관련 이슈는 항상 들어보실겁니다. 로컬환경에서 공공 API를 사용하거나, 웹 브라우저에서 CORS가 허용되지 않은 요청을 할때 많이 에러가 발생합니다.

아마 대부분 클라이언트 개발을 하며 많이들 경험하시는 에러일것입니다. 다양한 api를 가져다 쓰며 한번쯤은 경험했던 CORS 에러에 대해 궁금했었지만 제대로 알지는 못했었는데

오늘은 간단하게 CORS에 대해 정리를 해보겠습니다.


CORS란 뭔가요?

CORS는 교차 출처 리소스 공유(Cross-Origin Resource Sharing) 의 약자로 교차, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 정책입니다.

Cross야 그렇다 치는데.. 출처란 어떤의미인지 잘 모르신다면 지금 당장 크롬 개발자모드를 여시고, 콘솔창에 window.location.origin을 입력해 보시길 바랍니다.

// naver 에서 입력해본다면..
console.log(window.location.origin) // https://www.naver.com

그렇다면 url이 origin인 걸까요?
정답은 부분적으로는 맞다는 것입니다.

네이버 홈페이지에서 검색을 하고 네이버 검색페이지로 넘어가봅시다. 그 경우 url은 다음과 같은 값을 나타냅니다.

https://search.naver.com/search.naver?where=nexearch&sm=top_hty&fbm=0&ie=utf8&query=123123

이때 해당 url에서 유심히 봐야할 부분은 다음과 같습니다.

프로토콜 |     호스트      | 포트번호
https://search.naver.com:443

origin이란 프로토콜과 호스트(도메인), 그리고 포트번호를 합한값을 뜻합니다. 헌데, 일반적으로 우리는 http://localhost:3000 과 같은 포트번호를 붙은 url을 본적이 없을겁니다.

그 이유는 특별한 경우가 아닌 이상 http와 https 프로토콜의 경우 포트번호가 80과 443으로 고정되어 있기 때문입니다. 만약 포트번호를 직접 입력하거나, 다른 포트번호를 입력한다면 어떻게 될까요?

https://naver.com:443 // 잘 접속이 됩니다
https://naver.com:444 // 접속이 되지 않습니다

즉 이러하듯 브라우저는 각각의 origin을 가지고 있고, 다른(교차) origin(출처) 에 대한 요청(리소스)을 받아오는것(공유)에 대한 정책이 바로 우리가 겪고있는 CORS입니다.


CORS는 왜 있는걸까요?

CORS에 대해 정의를 간단하게 알아보았습니다. 그러면 이 정책이 왜 필요할까에 대한 의문점이 생깁니다. 그냥 이것저것 받아오게 하면 안되나 싶습니다.

하지만 만약, 다음과 같은 경우가 발생한다면 사용자는 큰 위험에 빠질 수 있습니다.

  1. A 브라우저에서 로그인과 같은 권환이 필요한 인증절차를 수행함
  2. 해커가 A 브라우저에서 개인정보를 가져와 자신에게 넘기는 명령을 만들어 유저에게 뿌림
  3. 한 유저가 모른채로 해당 명령어를 받아 수행함
  4. A 브라우저에서는 이미 권한이 있으므로, 유저의 개인정보가 해커에게 넘어감

정말 간단해보이지만 이러한 방식을 CSRF(Cross Site Request Forgery) 라고 하며, 이런 공격을 막기 위해 브라우저는 정책을 가지게 되는데, 이를 SOP라고 합니다.

SOP(Same-Origin Policy)는 2011년, 국제 인터넷 표준화 기구에서 RFC(Request for Comments) 6454에서 발표한 보안 정책으로 말 그대로 “같은 출처에서만 리소스를 공유할 수 있다”라는 규칙을 가진 정책입니다.
출처 : https://www.rfc-editor.org/rfc/rfc6454#page-5

위와 같은 공격을 막기위해 SOP정책을 세웠지만, 인터넷이 발달하며 다양한 리소스를 사용하는것은 불가피한 선택이 되었습니다. 이에 SOP에서도 다음과 같은 예외를 두기로 합니다.

  • script: 다른 출처(cross-origin)의 스크립트를 문서에 삽입(embed)하는 것은 가능하지만, fetch API 등을 이용하여 다른 출처로 요청을 날리는 것은 불가능합니다.
  • css: link 혹은 @import로 다른 출처의 css를 삽입할 수 있습니다. 이때 올바른 Content-Type 헤더가 설정되어야 할 수도 있습니다.
  • iframe: X-Frame-Options 응답 헤더가 DENY 혹은 SAMEORIGIN이 아닌 이상, 일반적으로 다른 출처의 iframe을 삽입하는 것은 가능합니다. 하지만 자바스크립트 등을 이용하여 다른 출처의 iframe에 접근하는 것은 불가능합니다.
  • form: form 태그의 action 속성값으로 출처가 다른 URL을 사용할 수 있습니다. 즉, 다른 출처로 폼 데이터를 전송하는 것이 가능합니다.
  • image: 다른 출처의 이미지를 삽입하는 것은 가능합니다. 하지만 자바스크립트 등을 이용하여 다른 출처의 이미지를 읽는 것은 불가능합니다 (자바스크립트를 이용하여 다른 출처의 이미지를 canvas에 삽입하는 경우)
  • multimedia: 다른 출처의 오디오·비디오를 삽입할 수 있습니다.
프로토콜 |     호스트      | 포트번호
https://search.naver.com:443

위에서 보았던 origin을 다시한번 가져와서 볼 경우 SOP는 프로토콜과 호스트, 포트번호가 모두 같은 출처에서 온 요청만을 허락합니다.

프로토콜 |     호스트      | 포트번호
https://search.naver.com:443/about  // origin 일치
http://search.naver.com:443         // 프로토콜 불일치
https://search.daum.com:443         // 호스트 불일치
https://search.naver.net:443        // 호스트 불일치
https://search.naver.com:131        // 포트번호 불일치

* origin 이후 패스나 파라미터를 통해 같은 출처에서 자원을 받습니다.
* IE의 경우 origin에서 포트번호를 보지 않는다고 합니다.

하지만 결국 다른 출처에 대한 요청이 불가피해지면서 이를 해결하기 위한 방법으로 CORS정책이 나오게 됩니다.

즉 SOP정책의 예외 중 하나의 케이스로 CORS정책이 존재하며, 이 덕분에 우리는 다른 출처에 대한 요청을 할수 있게 됩니다.


CORS는 어떻게 동작하나요?

기본적으로 웹 클라이언트 어플리케이션이 다른 출처의 리소스를 요청할 때는 HTTP 프로토콜을 사용하여 요청을 보내게 되는데, 이때 브라우저는 요청 헤더에 Origin이라는 필드에 요청을 보내는 출처를 함께 담아보냅니다.

이후 서버가 이 요청에 대한 응답을 할 때 응답 헤더의 Access-Control-Allow-Origin이라는 값에 “이 리소스를 접근하는 것이 허용된 출처 또는 *”를 내려주고, 이후 응답을 받은 브라우저는 자신이 보냈던 요청의 Origin과 서버가 보내준 응답의 Access-Control-Allow-Origin을 비교해본 후 이 응답이 유효한 응답인지 아닌지를 결정합니다.

보기에는 간단해 보이지만 조금 더 깊게 들어가면 CORS 요청은 다음과 같이 세 가지 유형으로 나눠질 수 있습니다.


프리플라이트 요청 (Preflight Request)

서버에 실제 요청을 보내기 전에 예비 요청에 해당하는 프리플라이트 요청(Preflight Request)을 먼저 보내서 실제 요청이 전송하기에 안전한지 확인하는 방법입니다.

예비요청이 안전하다면 본 요청을 서버에게 보내며 총 두번의 요청을 보내게 됩니다.

프리플라이트 요청의 특징은 다음과 같습니다.

  • 메소드로 OPTIONS를 사용합니다.
  • Origin 헤더에 자신의 Origin을 설정합니다.
  • Access-Control-Request-Method 헤더에 실제 요청에 사용할 메소드를 설정합니다.
  • Access-Control-Request-Headers 헤더에 실제 요청에 사용할 헤더들을 설정합니다.

이후 서버는 이러한 프리플라이트 요청에 대해 다음과 같은 특징을 가진 응답을 제공해야 합니다.

  • Access-Control-Allow-Origin 헤더에 허용되는 Origin들의 목록 혹은 와일드카드(*)를 설정합니다.
  • Access-Control-Allow-Methods 헤더에 허용되는 메소드들의 목록 혹은 와일드카드(*)를 설정합니다.
  • Access-Control-Allow-Headers 헤더에 허용되는 헤더들의 목록 혹은 와일드카드(*)를 설정합니다.
  • Access-Control-Max-Age 헤더에 해당 프리플라이트 요청이 브라우저에 캐시 될 수 있는 시간을 초 단위로 설정합니다.

이러한 응답을 받고 나면 브라우저는 이 응답의 정보를 자신이 전송한 요청의 정보와 비교하여 실제 요청의 안전성을 검사합니다. 예비 요청의 200응답이 오더라도 이 응답 이후 CORS를 판단하므로, 200응답과는 별개의 과정을 거칩니다.

만약 이 안전성 검사에 통과하게 된다면, 그때서야 실제 요청을 서버에게 보내게 됩니다. 이후 Access-Control-Request-XXX 형태의 헤더는 보내지 않습니다.


ex_screenshot

단순 요청 (Simple Request)

단순요청의 조건은 다음과 같습니다.

  • 메소드가 GET, HEAD, POST 중 하나여야 합니다.
  • User Agent가 자동으로 설정한 헤더를 제외하면, 아래와 같은 헤더들만 사용할 수 있습니다.

    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
    • DPR
    • Downlink (en-US)
    • Save-Data
    • Viewport-Width
    • Width
  • Content-Type 헤더에는 아래와 같은 값들만 설정할 수 있습니다.

    • application/x-www-form-urlencode
    • multipart/form-data
    • text/plain

위와 같은 조건을 만족할 경우 안전한 요청으로 취급하여 단순요청을 보낼수 있게 됩니다. 이 경우 프리플라이트 요청와는 다르게 단 한번의 요청만을 보내게 됩니다.


ex_screenshot

인증 정보를 포함한 요청 (Credentialed Request)

인증 정보(Credential)란 쿠키(Cookie) 혹은 Authorization 헤더에 설정하는 토큰 값 등을 말합니다. 만약 이러한 인증 정보를 함께 보내야 하는 요청(Credentialed Request)이라면, 별도로 따라줘야 하는 CORS 정책이 존재합니다.

XMLHttpRequest, jQuery의 ajax, 또는 axios를 사용한다면 withCredentials 옵션을 true로 설정해줘야 합니다.

fetch API를 사용한다면 credentials 옵션을 include로 설정해줘야 합니다.

이러한 별도의 설정을 해주지 않으면 쿠키 등의 인증 정보는 절대로 자동으로 서버에게 전송되지 않습니다

그리고 이 경우 서버의 응답 또한 위 경우와는 차이가 있습니다. 응답의 Access-Control-Allow-Origin 헤더가 와일드카드(*)가 아닌 분명한 Origin으로 설정되어야 하고, Access-Control-Allow-Credentials 헤더는 true로 설정되어야 합니다.

그렇지 않으면 브라우저에 의해 응답이 거부될 수 있습니다.


ex_screenshot


CORS는 어떻게 해결하나요?

서버의 경우

스프링의 경우 @CrossOrigin 어노테이션을 사용하고, 장고의 경우 django-cors-headers를 통해 특정 도메인이나 모든 요청에 대해 CORS 정책을 허용해 줄 수 있습니다. 이후 브라우저에서는 기본적으로 허용이 된 서버에서 약속한 도메인에서 요청을 호출하는 방법이 있습니다.

클라이언트의 경우

npm에서는 http-proxy-middleware같은 라이브러리를 이용하여 프록시 설정을 통해 브라우저를 속일수 있습니다. 가령 origin/api 에서 cross-origin/api에 대한 요청을 받고싶을 경우 앱이 cross-origin/api으로 요청을 프록싱해주어 CORS이슈를 회피할수 있습니다.

또는 내장 라이브러리중 fetch의 경우 mode : no-cors 라는 옵션이 있는데, 해당 옵션으로 요청을 보낼 경우 CORS정책을 무시하고 응답을 받게 됩니다. 다만 이 경우 “HEAD”, “GET”, 또는 “POST” 이외의 명령을 금지하며 브라우저는 Response로 전달되는 객체의 어떤값도 접근할수 없습니다.



참고
https://it-eldorado.tistory.com/163
https://evan-moon.github.io/2020/05/21/about-cors/#%EA%B0%99%EC%9D%80-%EC%B6%9C%EC%B2%98%EC%99%80-%EB%8B%A4%EB%A5%B8-%EC%B6%9C%EC%B2%98%EC%9D%98-%EA%B5%AC%EB%B6%84


Written by@JeongYeonJae
이것저것 쓰는 개발블로그

ResumeGitHub